安卓 Bitmap 高效加载,那些你必须掌握的稀碎知识点
↓推荐关注↓
Bitmap 是 Android 系统中的图像处理中最重要类之一。对于大多数 App,如何高效加载 Bitmap 显得至关重要。Bitmap 可以获取图像文件信息,对图像进行剪切、旋转、缩放,压缩等操作,并可以以指定格式保存图像文件。现在已经有很多主流的框架,如 Glide,Fresco,Picasso 等帮我们快速实现。其实这其中都包含了图片高效加载的策略,缓存策略等。这些框架当然很优秀,但有时候需要我们自己实现自己的图片加载框架的时候,我们也应该胸有成竹。
Bitmap 内存占用
Bitmap 的内存占用 = 宽 * 高 * 单位像素所占用字节,只与这三个因素有关,和图片是webp,png,jpg 没有关系。
Bitmap Config
颜色用 ARGB 来表示,A 表示 Alpha,即透明度;R 表示 red,即红色;G 表示 green,即绿色;B 表示 blue,即蓝色。Bitmap 的色彩也是用 ARGB 来表示的
Bitmap.Config中有Bitmap.Config.ALPHA_8、Bitmap.Config.RGB_565、Bitmap.Config.ARGB_4444、 Bitmap.Config.ARGB_8888有四个枚举变量。
Bitmap.Config.ALPHA_8表示:每个像素占8位,没有色彩,只有透明度A-8,共8位。Bitmap.Config.ARGB_4444表示:每个像素占16位,A-4,R-4,G-4,B-4,共4+4+4+4=16位。Bitmap.Config.RGB_565表示:每个像素占16位,没有透明度,R-5,G-6,B-5,共5+6+5=16位。Bitmap.Config.ARGB_8888表示:每个像素占32位,A-8,R-8,G-8,B-8,共8+8+8+8=32位。位数越高,那么可存储的颜色信息越多,图像也就越逼真。
drawable 不同分辨率文件夹放置和 Bitmap 大小的关系
具体的关系可以参考我的这篇文章Android 屏幕适配,那些你必须掌握的稀碎知识点。
减少内存占用
压缩,采样率 — inSampleSize,大于等于 1,通过对它的设置,对图片的像素的高和宽进行缩放,官方文档指出,inSampleSize 的取值应该总是 2 的指数,如 1,2,4,8 等。如果外界传入的 inSampleSize 的值不为 2 的指数,那么系统会向下取整并选择一个最接近 2 的指数来代替。同时图片不宜直接加载到内存中。
通常我们优化Bitmap时,当需要做性能优化或者防止 OOM(Out Of Memory),我们通常会使用Bitmap.Config.RGB_565 这个配置,因为Bitmap.Config.ALPHA_8 只有透明度,显示一般图片没有意义,Bitmap.Config.ARGB_4444 显示图片不清楚,Bitmap.Config.ARGB_8888 占用内存最多。
CompressFormat解析
Bitmap.CompressFormat.JPEG:表示以 JPEG 压缩算法进行图像压缩,压缩后的格式可以是".jpg"或者".jpeg",是一种有损压缩。
Bitmap.CompressFormat.PNG:表示以 PNG 压缩算法进行图像压缩,压缩后的格式可以是".png",是一种无损压缩。
Bitmap.CompressFormat.WEBP:表示以 WebP 压缩算法进行图像压缩,压缩后的格式可以是".webp",是一种有损压缩,质量相同的情况下,WebP 格式图像的体积要比 JPEG 格式图像小40%。美中不足的是,WebP 格式图像的编码时间“比 JPEG 格式图像长8倍”。
复用已经开辟出的内存,一般我们都会使用 LruCache(最近最少使用) 来缓存 Bitmap,当某个 Bitmap 被移除后,会调用 entryRemoved 方法进行移除,这个时候我们可以把这个移除的 Bitmap 放到复用池中进行复用(因为已经开辟出内存了),需要注意的是,我的复用池并没有设置大小,是因为每次
getReusable()
后都会将其移除,而且也不必担心复用池太大的问题(bitmap 很容易被复用,安卓系统的优化)。//复用方法
public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha, Bitmap reusable) {
Resources resources = context.getResources();
BitmapFactory.Options options = new BitmapFactory.Options();
// 设置为true后,再去解析,就只解析 out 参数
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources, id, options);
int w = options.outWidth;
int h = options.outHeight;
options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);
if (!hasAlpha) {
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
options.inJustDecodeBounds = false;
// 复用, inMutable 为true 表示易变
options.inMutable = true;
options.inBitmap = reusable;
return BitmapFactory.decodeResource(resources, id, options);
}
/**
* 3.0 之前不能复用
* 3.0-4.4 宽高一样,inSampleSize = 1
* 4.4 只要小于等于就行了
*
* @param w
* @param h
* @param inSampleSize
* @return
*/
public Bitmap getReusable(int w, int h, int inSampleSize) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return null;
}
Bitmap reusable = null;
Iterator<WeakReference<Bitmap>> iterator = reusablePool.iterator();
while (iterator.hasNext()) {
Bitmap bitmap = iterator.next().get();
if (bitmap != null) {
if (checkInBitmap(bitmap, w, h, inSampleSize)) {
reusable = bitmap;
iterator.remove();
break;
}
} else {
iterator.remove();
}
}
return reusable;
}
/**
* 校验bitmap 是否满足条件
*
* @param bitmap
* @param w
* @param h
* @param inSampleSize
* @return
*/
private boolean checkInBitmap(Bitmap bitmap, int w, int h, int inSampleSize) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return bitmap.getWidth() == w && bitmap.getHeight() == h && inSampleSize == 1;
}
if (inSampleSize > 1) {
w /= inSampleSize;
h /= inSampleSize;
}
int byteCount = w * h * getBytesPerPixel(bitmap.getConfig());
// 图片内存 系统分配内存
return byteCount <= bitmap.getAllocationByteCount();
}
内存加载策略
首先使用内存缓存 LruCache
使用复用缓存(内存缓存中 LruCache 中移除出来的,加入到复用缓存中)
使用磁盘缓存(DiskLruCache)
如果都找不到,再从网络获取(然后放入内存,放入磁盘)
private LruCache<String, Bitmap> lruCache;
private Set<WeakReference<Bitmap>> reusablePool;
private DiskLruCache diskLruCache;
public void init(Context context, String dir) {
// 复用池
reusablePool = Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());
// 内存大小
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = am.getMemoryClass();
//memoryClass * 1024 * 1024 / 8
lruCache = new LruCache<String, Bitmap>(memoryClass * 1024 * 1024 / 8) {
// 返回的一张图片大小
@Override
protected int sizeOf(String key, Bitmap value) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
return value.getAllocationByteCount();
}
return value.getByteCount();
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
if (oldValue.isMutable()) {
// 3.0 bitmap 缓存 native
// <8.0 bitmap 缓存 java
// 8.0 native
// 将bitmap 和一个队列进行关联(弱引用),当被回收的时候队列中会有该bitmap的引用,然后进行获取,回收bitmap
reusablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
} else {
oldValue.recycle(); //不可复用,就进行回收
}
}
};
try {
diskLruCache = DiskLruCache.open(new File(dir), BuildConfig.VERSION_CODE, 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
}
private ReferenceQueue<Bitmap> getReferenceQueue() {
if (referenceQueue == null) { //如果为空,就进行创建,创建一个线程,一直在轮询查找是否有被回收的bitmap 需要回收,前面调用reusablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue())); 将bitmap 和一个队列进行关联(弱引用),当被回收的时候队列中会有该bitmap的引用,然后进行获取,回收bitmap
referenceQueue = new ReferenceQueue<>();
new Thread(new Runnable() {
@Override
public void run() {
while (!shutDown) {
try {
Reference<? extends Bitmap> remove = referenceQueue.remove();//阻塞方法
Bitmap bitmap = remove.get();
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
return referenceQueue;
}
虽然在 android 8.0 之后,NativeAllocationRegistry可以辅助回收Java对象所申请的native 内存,但是通过上面轮询阻塞的方式,能够更快的进行内存回收。
内存回收
2.3~4.4 可以通过 inInputShareable、inPurgeable 让 Bitmap 的内存在 native 层分配(已废弃) 8.0 之前的 Bitmap 像素数据基本存储在 Java heap 8.0 之后的 Bitmap 像素数据基本存储在 native heap,NativeAllocationRegistry 是 Android 8.0 引入的一种辅助自动回收 native 内存的一种机制,当 Java 对象因为 GC 被回收后,NativeAllocationRegistry 可以辅助回收 Java 对象所申请的 native 内存 2.3 之前的像素存储需要的内存是在 native 上分配的,并且生命周期不太可控,可能需要用户自己回收。2.3-7.1 之间,Bitmap 的像素存储在 Dalvik 的 Java 堆上,当然,4.4 之前的甚至能在匿名共享内存上分配(Fresco 采用),而8.0 之后的像素内存又重新回到 native 上去分配,不需要用户主动回收,8.0 之后图像资源的管理更加优秀,极大降低了 OOM。
大图加载
BitmapRegionDecoder 大图加载利器
看看是横向还是纵向滑动,每次加载固定一小块区域(Rect),然后结合手势,判断是什么方向的移动,然后综合进行处理。
总结
Bitmap 其实是个很琐碎的知识点,涉及很多方面的知识,掌握好这些,后面我们在学习优秀的第三方框架也会更容易了,喜欢的点个赞吧~
转自:掘金 伤心的猪大肠
https://juejin.cn/post/6945227597274054664
- EOF -
看完本文有收获?请分享给更多人
推荐关注「安卓开发精选」,提升安卓开发技术
点赞和在看就是最大的支持❤️